package cn.toma.controller.sys;

import cn.toma.common.exception.FfBootException;
import cn.toma.common.utils.DateUtil;
import cn.toma.common.utils.web.AddressUtil;
import cn.toma.common.utils.web.CookieUtils;
import cn.toma.common.utils.web.IPUtil;
import cn.toma.common.vo.ActiveUser;
import cn.toma.common.vo.FfBootConstant;
import cn.toma.common.vo.StringPool;
import cn.toma.config.authentication.FfBootUtil;
import cn.toma.config.authentication.JWTToken;
import cn.toma.config.authentication.JWTUtil;
import cn.toma.config.web.utli.FfBootResponse;
import cn.toma.config.web.utli.MD5Util;
import cn.toma.dao.sys.LoginLogMapper;
import cn.toma.manager.sys.UserManager;
import cn.toma.model.sys.LoginLog;
import cn.toma.model.sys.User;
import cn.toma.properties.FfBootProperties;
import cn.toma.service.cache.RedisService;
import cn.toma.service.sys.LoginLogService;
import cn.toma.service.sys.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.*;

@Validated
@RestController
public class LoginController
{
	static String COOKIE_NAME = "CaptchaCode";

	@Autowired
	private RedisService redisService;
	@Autowired
	private UserManager userManager;
	@Autowired
	private UserService userService;
	@Autowired
	private LoginLogService loginLogService;
	@Resource
	private LoginLogMapper loginLogMapper;
	@Autowired
	private FfBootProperties properties;
	@Autowired
	private ObjectMapper mapper;

	@PostMapping("/login")
	public FfBootResponse login(
			@NotBlank(message = "{required}") String username, @NotBlank(message = "{required}") String code,
			@NotBlank(message = "{required}") String password, HttpServletRequest request) throws Exception
	{
		String cookieValue = CookieUtils.getCookieValue(request, COOKIE_NAME);

		if (cn.toma.common.utils.StringUtils.isEmpty(code) || cn.toma.common.utils.StringUtils.isEmpty(cookieValue)) {
			throw new FfBootException("请输入验证码");
		}

		String redisCode = redisService.get(cookieValue);
		if (cn.toma.common.utils.StringUtils.isEmpty(redisCode)) {
			throw new FfBootException("验证码过期");
		}
		if (redisCode.compareToIgnoreCase(code) != 0) {
			throw new FfBootException("验证码有误");
		}

		redisService.del(cookieValue);

		username = StringUtils.lowerCase(username);
		password = MD5Util.encrypt(username, password);

		final String errorMessage = "用户名或密码错误";
		User user = this.userManager.getUser(username);

		if (user == null) {
			throw new FfBootException(errorMessage);
		}
		if (!StringUtils.equals(user.getPassword(), password)) {
			throw new FfBootException(errorMessage);
		}
		if (User.STATUS_LOCK.equals(user.getStatus())) {
			throw new FfBootException("账号已被锁定,请联系管理员！");
		}

		// 更新用户登录时间
		this.userService.updateLoginTime(username);
		// 保存登录记录
		LoginLog loginLog = new LoginLog();
		loginLog.setUsername(username);
		loginLog.setSystemBrowserInfo(request.getHeader("user-agent"));
		this.loginLogService.saveLoginLog(loginLog);

		String token = FfBootUtil.encryptToken(JWTUtil.sign(username, password));
		LocalDateTime expireTime = LocalDateTime.now().plusSeconds(properties.getShiro().getJwtTimeOut());
		String expireTimeStr = DateUtil.formatFullTime(expireTime);
		JWTToken jwtToken = new JWTToken(token, expireTimeStr);

		String userId = this.saveTokenToRedis(user, jwtToken, request);
		user.setId(userId);

		Map<String, Object> userInfo = this.generateUserInfo(jwtToken, user);
		return new FfBootResponse().message("认证成功").data(userInfo);
	}

	@GetMapping("index/{username}")
	public FfBootResponse index(@NotBlank(message = "{required}") @PathVariable String username)
	{
		Map<String, Object> data = new HashMap<>();
		// 获取系统访问记录
		Long totalVisitCount = loginLogMapper.findTotalVisitCount();
		data.put("totalVisitCount", totalVisitCount);
		Long todayVisitCount = loginLogMapper.findTodayVisitCount();
		data.put("todayVisitCount", todayVisitCount);
		Long todayIp = loginLogMapper.findTodayIp();
		data.put("todayIp", todayIp);
		// 获取近期系统访问记录
		List<Map<String, Object>> lastSevenVisitCount = loginLogMapper.findLastSevenDaysVisitCount(null);
		data.put("lastSevenVisitCount", lastSevenVisitCount);
		User param = new User();
		param.setUsername(username);
		List<Map<String, Object>> lastSevenUserVisitCount = loginLogMapper.findLastSevenDaysVisitCount(param);
		data.put("lastSevenUserVisitCount", lastSevenUserVisitCount);
		return new FfBootResponse().data(data);
	}

	@RequiresPermissions("user:online")
	@GetMapping("online")
	public FfBootResponse userOnline(String username) throws Exception
	{
		String now = DateUtil.formatFullTime(LocalDateTime.now());
		Set<String> userOnlineStringSet = redisService.zrangeByScore(FfBootConstant.ACTIVE_USERS_ZSET_PREFIX, now, "+inf");
		List<ActiveUser> activeUsers = new ArrayList<>();
		for (String userOnlineString : userOnlineStringSet) {
			ActiveUser activeUser = mapper.readValue(userOnlineString, ActiveUser.class);
			activeUser.setToken(null);
			if (StringUtils.isNotBlank(username)) {
				if (StringUtils.equalsIgnoreCase(username, activeUser.getUsername())) {
					activeUsers.add(activeUser);
				}
			}
			else {
				activeUsers.add(activeUser);
			}
		}
		return new FfBootResponse().data(activeUsers);
	}

	@DeleteMapping("kickout/{id}")
	@RequiresPermissions("user:kickout")
	public void kickout(@NotBlank(message = "{required}") @PathVariable String id) throws Exception
	{
		String now = DateUtil.formatFullTime(LocalDateTime.now());
		Set<String> userOnlineStringSet = redisService.zrangeByScore(FfBootConstant.ACTIVE_USERS_ZSET_PREFIX, now, "+inf");
		ActiveUser kickoutUser = null;
		String kickoutUserString = "";
		for (String userOnlineString : userOnlineStringSet) {
			ActiveUser activeUser = mapper.readValue(userOnlineString, ActiveUser.class);
			if (StringUtils.equals(activeUser.getId(), id)) {
				kickoutUser = activeUser;
				kickoutUserString = userOnlineString;
			}
		}
		if (kickoutUser != null && StringUtils.isNotBlank(kickoutUserString)) {
			// 删除 zset中的记录
			redisService.zrem(FfBootConstant.ACTIVE_USERS_ZSET_PREFIX, kickoutUserString);
			// 删除对应的 token缓存
			redisService.del(FfBootConstant.TOKEN_CACHE_PREFIX + kickoutUser.getToken() + "." + kickoutUser.getIp());
		}
	}

	@GetMapping("logout/{id}")
	public void logout(@NotBlank(message = "{required}") @PathVariable String id) throws Exception
	{
		this.kickout(id);
	}

	@PostMapping("regist")
	public void regist(
			@NotBlank(message = "{required}") String username,
			@NotBlank(message = "{required}") String password) throws Exception
	{
		String encrypt = MD5Util.encrypt(username, password);
		this.userService.regist(username, encrypt);
	}

	private String saveTokenToRedis(User user, JWTToken token, HttpServletRequest request) throws Exception
	{
		String ip = IPUtil.getIpAddr(request);

		// 构建在线用户
		ActiveUser activeUser = new ActiveUser();
		activeUser.setUsername(user.getUsername());
		activeUser.setIp(ip);
		activeUser.setToken(token.getToken());
		activeUser.setLoginAddress(AddressUtil.getCityInfo(ip));

		// zset 存储登录用户，score 为过期时间戳
		this.redisService.zadd(FfBootConstant.ACTIVE_USERS_ZSET_PREFIX, Double.valueOf(token.getExipreAt()), mapper.writeValueAsString(activeUser));
		// redis 中存储这个加密 token，key = 前缀 + 加密 token + .ip
		this.redisService.set(FfBootConstant.TOKEN_CACHE_PREFIX + token.getToken() + StringPool.DOT + ip, token.getToken(), properties.getShiro().getJwtTimeOut() * 1000);

		return activeUser.getId();
	}

	/**
	 * 生成前端需要的用户信息，包括：
	 * 1. token
	 * 2. Vue Router
	 * 3. 用户角色
	 * 4. 用户权限
	 * 5. 前端系统个性化配置信息
	 *
	 * @param token token
	 * @param user  用户信息
	 * @return UserInfo
	 */
	private Map<String, Object> generateUserInfo(JWTToken token, User user)
	{
		String username = user.getUsername();
		Map<String, Object> userInfo = new HashMap<>();
		userInfo.put("token", token.getToken());
		userInfo.put("exipreTime", token.getExipreAt());

		Set<String> roles = this.userManager.getUserRoles(username);
		userInfo.put("roles", roles);

		Set<String> permissions = this.userManager.getUserPermissions(username);
		userInfo.put("permissions", permissions);


		user.setPassword("it's a secret");
		userInfo.put("user", user);
		return userInfo;
	}
}
